package net.davidlauzon.assemblandroid;

import java.util.ArrayList;

import android.app.Application;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;

import net.davidlauzon.assemblandroid.activities.DialogThemedActivity;
import net.davidlauzon.assemblandroid.activities.IExceptionManager;
import net.davidlauzon.assemblandroid.exceptions.AssemblaAPIException;
import net.davidlauzon.assemblandroid.exceptions.XMLParsingException;
import net.davidlauzon.assemblandroid.models.Space;
import net.davidlauzon.assemblandroid.models.Task;
import net.davidlauzon.assemblandroid.models.Task.State;
import net.davidlauzon.assemblandroid.models.Ticket;
import net.davidlauzon.net.RestfulException;
import net.davidlauzon.utils.C;
import net.davidlauzon.utils.IObservable;
import net.davidlauzon.utils.IObserver;
import net.davidlauzon.utils.MyTimer;

/**
 * GRASP Controller for all the time entry tasks
 * 
 * @author david
 */
public class TimeTrackerApplication extends Application implements IExceptionManager {
	static private final String TAG = "AssemblaTT";
	
	// Singleton Pattern
	static private TimeTrackerApplication instance;
	static public  TimeTrackerApplication getInstance() { return instance; }
	
	
	private AssemblaAPIAdapter assemblaAdapter;
	
	private ArrayList<Space> spaces;
	
	private Space currentSpace;
	private Ticket currentTicket;
	private Task currentTask;

	private SharedPreferences preferences;
	
	
	/**
	 * SINGLETON: A private constructor enforces the Singleton pattern by preventing external instantiation.
	 * (EDIT: damn, Android needs a public constructor to initiate it)
	 */
	public TimeTrackerApplication() {
		Log.i(TAG, "new TimeTrackerApplication");
		
		if (instance != null)
			Log.w(TAG, "WARNING: TimeTrackerApplication instanciated more than once! "
					+ "Now using last created instance.\n ALL EXISTING VALUES WILL BE LOST");
		
		instance = this;
	}
	
	/**
	 * Manages a non-fatal Exception
	 * @param Exception e
	 */
	
	public void manageException(Exception e) 
	{
		Log.e(TAG, e.toString() );
		e.printStackTrace();
		
		// TODO: devrait le déduire du Exception lui-même
		boolean fatal = false;
		if(e instanceof XMLParsingException && ((XMLParsingException)e).getFatal())
			fatal = true;
		
		String title = (fatal ? "FATAL: " : "");
		
		if(e instanceof RestfulException)
			title += "Communication  Error";
		else if(e instanceof XMLParsingException)
			title += "XML Parsing Error";
		else if(e instanceof AssemblaAPIException)
			title += "Unexpected Answer Error";

		else
			title += e.getClass().getSimpleName();
		
			
		// Show Error Dialog
		Intent intent = new Intent(this, DialogThemedActivity.class);
		intent.putExtra("title", title );
		intent.putExtra("message", e.toString() );
		
		if (fatal) {
			intent.putExtra("fatal", true);
			//Log.e(TAG, "Fatal Exception, app will now closes");
		}
		
		intent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
		startActivity( intent );
		errorDialogActive = true;
	}
	
	private boolean errorDialogActive = false;
	
	/**
	 * Manage the exception for the application.
	 * 
	 * @param e Exception
	 * @param fatal If fatal the exception is not rescued
	 */
	
	public void onErrorDialogQuit() {
		errorDialogActive = false;
	}
	
	
	/**
	 * Created when this class is instanciated by the Android environment
	 * 
	 * http://stackoverflow.com/questions/456211/activity-restart-on-rotation-android
	 */
	@Override
	public void onCreate() {
		super.onCreate();
		Log.i(TAG, "TimeTrackingApplication starting...");
		
		try {
			assemblaAdapter = AssemblaAPIAdapter.getInstance();

		} catch (XMLParsingException e) {
			// FATAL Exception
			manageException(e);
			return;
		}
		
		// Load preferences
		this.preferences = PreferenceManager.getDefaultSharedPreferences(this);
		
		reloadPreferences();
	}
	
	public void reloadPreferences() {
		String username  = this.preferences.getString("username", "");
		String password  = this.preferences.getString("password", "");
		
		//Log.i(TAG, "TimeTrackingManager.setPreferences() user=" + username + "; pass=" + password);
		try {
			assemblaAdapter.setCredentials(username, password);
		} catch (Exception e) {
			manageException(e);
		}
	}
	
	public ArrayList<String> projectNamesForList() {
		
		ArrayList<String> projectNames = new ArrayList<String>();
        
        for (Space space : this.spaces) {
        	projectNames.add(space.listItemText());
    	}
        
        return projectNames;
	}
	
	public ArrayList<Ticket> ticketsForList() {
		return currentSpace.getTickets();
	}
	
	public void selectProject(int position) {
		currentSpace = spaces.get(position);
	}
	
	public Space curProject() {
		return currentSpace;
	}
	
	public void selectTicket(int position) {
		if (currentSpace != null) {
			currentTicket = currentSpace.getTickets().get(position);
		}
	}
	
	public Ticket curTicket() {
		return currentTicket;
	}
	
	
	public Task startTimeEntry() {
		currentTask = new Task(currentSpace.id(), currentTicket.id());
		currentTask.start();
		Log.i(TAG, "Time entry starting for: Ticket #" + 
				Integer.toString(currentTicket.id()) + " '" + currentTicket.name() + 
				"' (Space = '" + currentSpace.name() + "')");
		
		return currentTask;
	}
	
	public Task curTask() {
		return currentTask;
	}
	
	/**
	 * @return current paused state (true = paused)
	 */
	public State togglePauseTimeEntry() {
		switch (currentTask.state()) {
			case STARTED: currentTask.pause(); break;
			case STOPPED: currentTask.resume(); break;
			case PAUSED:  currentTask.resume(); break;
		}
		return currentTask.state();
	}
	
	public void stopTimeEntry() {
		currentTask.stop();
	}
	
	
	
	/*************************************************************************
	 * Projects Loading Task
	 *************************************************************************/
	
	private ProjectsLoadingTask projectsTask;
	
	public void fetchProjectsLater(IObserver observer) {
		this.projectsTask = new ProjectsLoadingTask(observer);
		this.projectsTask.execute();
	}
    
    private void onProjectsLoaded() {
    	//Log.i(TAG, "dispatchProjectsLoaded");
    	// Free memory
    	projectsTask.cancel(true);
    	projectsTask = null;
    }
    
    /**
     * @return true if projects have been loaded already.
     */
    public boolean projectsReady() {
    	return (spaces == null) ? false : true;
    }
	
    
    private class ProjectsLoadingTask extends AsyncTask<Void, Void, ArrayList<Space>> implements IObservable
    {
    	private ArrayList<IObserver> observers = new ArrayList<IObserver>();
    	
    	private Exception exception = null;
    	
    	public ProjectsLoadingTask(IObserver observer) {
    		super();
    		addObserver(observer);
    	}
    	
    	@Override
		protected ArrayList<Space> doInBackground(Void... params) {
			//Log.i(TAG, "doInBackground");
			MyTimer.start("loadingSpaces");
	        try {
				spaces = assemblaAdapter.getMySpaces();
			} catch (Exception e) {
				exception = e;
				spaces = null;
			}
	        MyTimer.stop("loadingSpaces");
	        Log.i(TAG, "Spaces loaded in " + MyTimer.get("loadingSpaces") + " ; parsed in " + MyTimer.get("XMLParsingSpaces") );
			
			return spaces;
		}
		
		protected void onPostExecute(ArrayList<Space> spaces) {
			//Log.i(TAG, "onPostExecute");
			
			notifyObservers();
			clearObservers();
			onProjectsLoaded();
			
			if (spaces != null) {
		        Toast.makeText( TimeTrackerApplication.this, 
	    				String.format("Found %d projects for you.", spaces.size() ), 
	    				Toast.LENGTH_SHORT).show();
			}
		}

		public void addObserver(IObserver observer) {
			observers.add(observer);
		}

		public void removeObserver(IObserver observer) {
			observers.remove(observer);
		}

		public void notifyObservers() {
			for (IObserver observer : observers) {
				if (exception == null) {
					observer.onUpdate();
				} else {
					manageException(exception);
					observer.onUpdateFailed(exception);
				}
			}
		}
		
		public void clearObservers() {
			observers.clear();
		}
    }
	
    
    
    
    /*************************************************************************
	 * Tickets Loading Task
	 *************************************************************************/
	TicketsLoadingTask ticketsTask;
	public void fetchTicketsLater(IObserver observer) {
		this.ticketsTask = new TicketsLoadingTask(observer, currentSpace);
		this.ticketsTask.execute();
	}
    
    private void onTicketsLoaded() {
    	//Log.i(TAG, "dispatchTasksLoaded");
    	// Free memory
    	ticketsTask.cancel(true);
    	ticketsTask = null;
    }
    
    /**
     * @return true if a space has been selected, and its tickets have been loaded already.
     */
    public boolean ticketsReady() {
    	if (currentSpace != null && currentSpace.ticketsLoaded())
    		return true;
    	else
    		return false;
    }
	
    private class TicketsLoadingTask extends AsyncTask<Void, Void, ArrayList<Ticket>> implements IObservable
    {
    	private ArrayList<IObserver> observers = new ArrayList<IObserver>();
    	private Space space = null;
    	
    	private Exception exception = null;
    	
    	public TicketsLoadingTask(IObserver observer, Space space) {
    		super();
    		this.space = space;
    		addObserver(observer);
    	}
    	
    	@Override
		protected ArrayList<Ticket> doInBackground(Void... params) {
			//Log.i(TAG, "doInBackground");
    		
    		MyTimer.start("loadingTickets");
    		ArrayList<Ticket> tickets;
			try {
				tickets = space.reloadTickets(false, false);
			} catch (Exception e) {
				// Wait for onPostExecute() to throw the exception in the UI thread
				// http://stackoverflow.com/questions/1739515/android-asynctask-and-error-handling
				this.exception = e;
				tickets = null;
			}
          	MyTimer.stop("loadingTickets");
	        Log.i(TAG, "Tickets loaded in " + MyTimer.get("loadingTickets") + " ; parsed in " + MyTimer.get("XMLParsingTickets") );
	        
			return tickets;
		}
		
    	/**
    	 * NOTE: this method might be broken if an exception is triggered
    	 */
		protected void onPostExecute(ArrayList<Ticket> tickets) {
			//Log.i(TAG, "onPostExecute");
			
			notifyObservers();
			clearObservers();
			onTicketsLoaded();
			
			if (tickets != null) {
		        int totalTickets = tickets.size() + assemblaAdapter.skippedTickets();
		        Toast.makeText( TimeTrackerApplication.this, 
	    				String.format("Found %d/%d tickets available.", tickets.size(), totalTickets), 
	    				Toast.LENGTH_SHORT).show();
			}
		}

		public void addObserver(IObserver observer) {
			observers.add(observer);
		}

		public void removeObserver(IObserver observer) {
			observers.remove(observer);
		}

		public void notifyObservers() {
			for (IObserver observer : observers) {
				if (exception == null) {
					observer.onUpdate();
				} else {
					manageException(exception);
					observer.onUpdateFailed(exception);
				}
			}
		}
		
		public void clearObservers() {
			observers.clear();
		}
    }
    
    
    
    /*************************************************************************
	 * Save Time Entry Task
	 *************************************************************************/
    SaveTimeEntryTask saveTimeEntryTask;

	public void saveTimeEntryLater(String description, boolean markToTest, IObserver observer) {
    	//Log.i(TAG, "saveTimeEntryLater");

		// Allow to save a task with an empty description
		if (description.length() == 0) {
			description = ".";
		}
		
		currentTask.setDescription( description );
		
		//TODO: FIXME
		// 2. Update the project
		Ticket ticket = currentTicket;
		ticket.addWorkingHours( currentTask.elapsedHours() );
		ticket.setAssignedToId( assemblaAdapter.getUserId() );   // ugly ;-)
		ticket.setLastLogMessage( description );
		
		if (markToTest) {
			ticket.setStatus( Ticket.STATUS_TEST );
		} else if (ticket.status() == Ticket.STATUS_NEW) {
			ticket.setStatus( Ticket.STATUS_ACCEPTED );
		}
		
		
		this.saveTimeEntryTask = new SaveTimeEntryTask(observer, currentTask, currentTicket, currentSpace);
		this.saveTimeEntryTask.execute();
	}
    
    private void onTimeEntrySaved() {
    	//Log.i(TAG, "onTimeEntrySaved");
    	saveTimeEntryTask.cancel(true);
    	saveTimeEntryTask = null;
    }
    

    private class SaveTimeEntryTask extends AsyncTask<Void, Void, Void> implements IObservable
    {
    	private ArrayList<IObserver> observers = new ArrayList<IObserver>();
		private Space  space  = null;
		private Ticket ticket = null;
    	private Task   task   = null;
    	
    	private Exception exception = null;

    	
    	public SaveTimeEntryTask(IObserver observer, Task task, Ticket ticket, Space space) {
    		super();
    		//Log.i(TAG, "new SaveTimeEntryTask");
    		this.task = task;
    		this.ticket = ticket;
    		this.space = space;
    		
    		addObserver(observer);
    	}
    	
    	@Override
		protected Void doInBackground(Void... params) {
			//Log.i(TAG, "doInBackground");
			try {
				assemblaAdapter.saveTimeEntry(space, ticket, task);
			} catch (Exception e) {
				exception = e;
			}
			return null;
		}
		
		protected void onPostExecute(Void param) {
			//Log.i(TAG, "SaveTimeEntryTask onPostExecute");
			notifyObservers();
			clearObservers();
			onTimeEntrySaved();
		}

		public void addObserver(IObserver observer) {
			observers.add(observer);
		}

		public void removeObserver(IObserver observer) {
			observers.remove(observer);
		}

		public void notifyObservers() {
			for (IObserver observer : observers) {
				if (exception == null) {
					observer.onUpdate();
				} else {
					manageException(exception);
					observer.onUpdateFailed(exception);
				}
			}
		}
		
		public void clearObservers() {
			observers.clear();
		}
    }
}
